#!/usr/bin/env python

"""PairProgramming.py

ToDo: allow dynamic addition of new people to the list at any time

Generates an HTML file called PairProgramming.html which uses Javascript and
DHTML to move a highlight bar through a list of name pairs. These name pairs are
randomly chosen starting from the contents of the file "GroupList.txt"
(containing one name per line). An algorithm tries to prevent duplicate
pairings.

The pair information is stored in PairProgramming.dic. If you want to start over
(for example, if the program seems to be taking too long, which it will do when
it's too hard to find new unique matches), simply delete the file
PairProgramming.dic.

New features:

-- Checks for hangups and restarts match process if more than 100 retries occur

-- Automatically opens the resulting file in a browser

-- Matches "<+>" attendees with anyone who is not "<+>"

-- Excludes "<Assistant>" from matches

-- Now uses GroupList.txt to get names for each shuffle. If someone isn't there
   you can temporarily remove their name by putting a '#' at the beginning of that line
   (all names must be on the list the first time)

-- Removes the last element of each line in GroupList.txt, so GroupList.txt can be a
   full class roster with names and email addresses.

-- Uses 2.1+ Random instead of deprecated whrandom, and the shuffle() function

-- Uses Unicode to handle accented and non-latin characters. This assumes that the
   GroupList file is encoded in UTF-8. Generated HTML also uses UTF-8 encoding.

"""
import sys, random, string, os, webbrowser, copy

GroupList = "GroupList.txt"
baseName = 'PairProgramming'
htmlFile = baseName + '.html'

page = '''
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Bruce Eckel's Pair Programming pair-up tool</title>
<script language="javascript">
pause = 3000

people = new Array(%s)

numPairs = %s

document.write("<style type='text/css'>")
for(i = 1; i <= numPairs; i++)
  document.write("#hot" + i + " {color:black; background:green}")
document.write("</style>");

currLine = 1
function cycleColors() {
  for(i = 1; i <= numPairs; i++) {
    obj = eval("document.all.hot" + i)
    obj.style.color = (i == currLine)? "white":"black"
    obj.style.background = (i == currLine)? "green":"white"
  }
  currLine++;
  if(currLine > numPairs) currLine = 1;
  setTimeout("cycleColors()", pause)
}
</script>
</head>
<body onLoad="cycleColors()">
<font FACE="Verdana,Tahoma,Arial,Helvetica,Sans">
<center>
<TABLE BORDER="4" CELLPADDING="2" CELLSPACING="0">
<script>
x = 0
for(i = 1; i <= numPairs; i++) {
  document.write("<TR id=hot" + i + "><TD><b><font size='+2'>" +  people[x++] +
    "</font></b></TD><TD><b><font size='+2'>" + people[x++] + "</font></b></TD></TR>")
}
</script>
</TABLE></body></html>
'''

'''
pairs = {
  'name1': [], # list of people previously paired with
  'name2': [],
  # ...
}
'''

def parseList(fileName):
  list = [line.strip().decode('utf-8') 
            for line in file(fileName) if not line.startswith('#')]
  list2 = []
  for line in list:
    words = line.split()
    if words[-1].find(u'@') != -1:
      list2.append(u' '.join(words[0:-1]))
    else:
      list2.append(line)
  low = []
  medium = []
  for line in list2:
    if line.find(u'<Assistant>') != -1: continue
    if line.find(u'<+>') != -1:
        low.append(line)
    else:
        medium.append(line)
  return (low, medium)

def bothLow(partner1, partner2):
  return partner1.find(u'<+>') != -1 and partner2.find(u'<+>') != -1

def makePairs(list, pairs):
    """ Make deep copy of list and pairs, in case of failure """
    originalList = list[:]
    originalPairs = copy.deepcopy(pairs)
    list = list[:]
    pairs = copy.deepcopy(pairs)
    retries = 0
    random.seed()
    # No shuffling; start with low members first to ensure matching low & med
    pairlist = []
    while list:
        name = list.pop()
        if not list: # last element in list, odd person out: pair with a group
            for i in range(len(pairlist)):
                grp = pairlist[i]
                if name not in pairs[grp[0]] and name not in pairs[grp[1]]:
                    pairs[name].append(grp[0])
                    pairs[name].append(grp[1])
                    pairs[grp[0]].append(name)
                    pairs[grp[1]].append(name)
                    pairlist[i] = (grp[0], grp[1] + u', ' + name)
                    break
            break
        partner = random.choice(list)
        while bothLow(name, partner) or pairs[name] and partner in pairs[name]:
            #print name.split("<")[0] + " failed match with " + partner.split("<")[0]
            partner = random.choice(list)
            retries += 1
            if retries > 100:
                #print "Too many retries -- recursive restart"
                print "Restarting..."
                return makePairs(originalList, originalPairs)
        pairs[name].append(partner) # Add new partner to end of previous pairs
        pairs[partner].append(name) # Update partner's list, too
        pairlist.append((name, partner))
        list.remove(partner)
    npairlist = []
    for p1, p2 in pairlist:
        npairlist.append((p1.split(u"<")[0], p2.split(u"<")[0]))
    return npairlist, pairs

if __name__ == "__main__":
  if not os.path.exists(GroupList):
    print __doc__
    sys.exit(1)
  low, medium = parseList(GroupList)
  # Last name in each line must be email address, not used here:
  #nameList = [' '.join(l.split()[:-1]) for l in file(GroupList) if not l[0].startswith('#')]
  try:
    pairs = eval(file(baseName + '.dic').read())
  except: # Hasn't been mapped yet
    pairs = {}
    for rawName in low + medium:
      pairs[rawName] = []
  list = medium + low # makes a deep copy
  #for name, matches in pairs.items():
  #    print name.split("<")[0], len(matches)
  pairlist, pairs = makePairs(list, pairs)
  file(baseName + '.dic', 'w').write(string.replace(u'%s' % pairs, u'],', u'],\n'))
  prs = []
  for p in pairlist:
    prs.append(p[0].encode('utf-8'))
    other = p[1].replace(u', ', u',<br />') # line break to make group easier to spot
    prs.append(other.encode('utf-8'))
  jspairs = '"%s"' % '", "'.join(prs)
  open(htmlFile, 'w').write(page % (jspairs, len(pairlist)))

webbrowser.open("file:///" + os.path.abspath('.').replace('\\', '/') + "/" + htmlFile);

